import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;

/**
 * AES加密 解密 工具类
 *
 * @author dongyang
 * @since 2019-04-23 10:51
 **/
public class AesEcbUtils {

    private static final String UTF8_ENCODING = "UTF-8";

    private static final int ENCRYPT_KEY_SPLIT_LENGTH = 2;

    private String encryptKey;

    private String fixedEncryptKey;

    private String encryptKeyType;

    public AesEcbUtils(String encryptKey) {
        // encryptKey like this  1:6a6c4f66df581405768223954baae2e3
        if (StringUtils.isBlank(encryptKey)) {
            throw new IllegalArgumentException("encryptKey is blank");
        }
        final String[] sections = encryptKey.split(":");
        if (sections.length != ENCRYPT_KEY_SPLIT_LENGTH) {
            throw new IllegalArgumentException("illegal encryptKey");
        }
        this.encryptKey = encryptKey;
        this.encryptKeyType = sections[0];
        this.fixedEncryptKey = sections[1];
    }

    /**
     * 字段加密
     *
     * @param content 加密字段内容
     * @param <T>     字段类型
     * @return 加密后的字段内容
     */
    public <T> String encrypt(T content) {
        String contentStr;
        if (content instanceof String) {
            contentStr = (String) content;
        } else {
            contentStr = JSONObject.toJSONString(content);
        }
        return this.encrypt(encryptKey, contentStr);
    }

    /**
     * 解密字段
     *
     * @param encryptedContent 加密的字段内容
     * @param type             字段类型
     * @param <T>              字段类型
     * @return 解密后的字段内容
     */
    public <T> T decrypt(String encryptedContent, Class<T> type) {
        String content = decrypt(encryptKey, encryptedContent);

        return JSONObject.parseObject(content, type);
    }

    private String decrypt(String encKey, final String content) {
        if (StringUtils.isBlank(encKey) || StringUtils.isBlank(content)) {
            throw new IllegalArgumentException("varEncKey or content blank");
        }
        String md5EncKey = getMd5Digest(encryptKey, null);
        String value = content.split(":")[1];
        try {
            final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
            byte[] aesKeys = this.getAesCipherKey(fixedEncryptKey, md5EncKey);
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKeys, "AES"));
            byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(value));
            // clear padding char
            decrypted = Pkcs7Encoder.decode(decrypted);
            return new String(decrypted, UTF8_ENCODING);
        } catch (Exception e) {
            throw new IllegalStateException("failed to decrypt the secure field.", e);
        }
    }

    private String encrypt(String encKey, final String content) {
        if (StringUtils.isBlank(encKey) || StringUtils.isBlank(content)) {
            throw new IllegalArgumentException("md5EncKey or content blank");
        }
        String md5EncKey = getMd5Digest(this.encryptKey, null);
        try {
            final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
            byte[] aesKeys = this.getAesCipherKey(fixedEncryptKey, md5EncKey);
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKeys, "AES"));
            //　add byte[] to group
            ByteGroup byteGroup = new ByteGroup();
            byteGroup.addBytes(content.getBytes(UTF8_ENCODING));
            // ... + pad: do padding char
            byteGroup.addBytes(Pkcs7Encoder.encode(byteGroup.size()));
            //
            byte[] encrypted = cipher.doFinal(byteGroup.toBytes());
            return String.format("%s:%s", encryptKeyType, Base64.getEncoder().encodeToString(encrypted));
        } catch (Exception e) {
            throw new IllegalStateException("failed to encrypt the secure field.", e);
        }
    }

    public static class ByteGroup {

        private final List<Byte> byteContainer = new ArrayList<Byte>();

        private ByteGroup() {
        }

        private byte[] toBytes() {
            byte[] bytes = new byte[this.byteContainer.size()];
            for (int i = 0; i < this.byteContainer.size(); ++i) {
                bytes[i] = this.byteContainer.get(i);
            }

            return bytes;
        }

        private void addBytes(byte[] bytes) {
            for (byte b : bytes) {
                this.byteContainer.add(b);
            }
        }

        public int size() {
            return this.byteContainer.size();
        }
    }

    public static class Pkcs7Encoder {

        private static final int BLOCK_SIZE = 32;

        private Pkcs7Encoder() {
        }

        public static byte[] encode(int count) throws UnsupportedEncodingException {
            int amountToPad = BLOCK_SIZE - count % BLOCK_SIZE;
            if (amountToPad == 0) {
                amountToPad = BLOCK_SIZE;
            }
            char padChr = (char) (amountToPad & 255);
            StringBuilder tmp = new StringBuilder();
            for (int index = 0; index < amountToPad; ++index) {
                tmp.append(padChr);
            }

            return tmp.toString().getBytes(UTF8_ENCODING);
        }

        private static byte[] decode(byte[] decrypted) {
            int pad = decrypted[decrypted.length - 1];
            if (pad < 1 || pad > BLOCK_SIZE) {
                pad = 0;
            }

            return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
        }
    }

    private byte[] getAesCipherKey(final String fixedEncKey, final String varEncKey) {
        String key = fixedEncKey + varEncKey;
        try {
            return getMd5Digest(key, 16).getBytes(UTF8_ENCODING);
        } catch (Exception e) {
            throw new IllegalStateException("getAesCipherKey error", e);
        }
    }

    private String getMd5Digest(final String key, Integer subStringLength) {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.reset();
            digest.update(key.getBytes("UTF-8"));
            if (null != subStringLength && subStringLength.compareTo(0) > 0) {
                return Hex.encodeHexString(digest.digest()).substring(0, subStringLength);
            }
            return Hex.encodeHexString(digest.digest());
        } catch (Exception e) {
            throw new IllegalStateException("NoSuchAlgorithmException,stop!", e);
        }
    }

    public String getEncryptKey() {
        return encryptKey;
    }

    public void setEncryptKey(String encryptKey) {
        this.encryptKey = encryptKey;
    }

    public String getFixedEncryptKey() {
        return fixedEncryptKey;
    }

    public void setFixedEncryptKey(String fixedEncryptKey) {
        this.fixedEncryptKey = fixedEncryptKey;
    }

    public String getEncryptKeyType() {
        return encryptKeyType;
    }

    public void setEncryptKeyType(String encryptKeyType) {
        this.encryptKeyType = encryptKeyType;
    }
}
